Microtasks and macrotasks are two types of task queues in JavaScript's event loop, with microtasks having higher priority and being executed immediately after the current operation completes, before any macrotasks like rendering or I/O callbacks.
JavaScript's event loop maintains multiple queues for handling asynchronous operations, with the most fundamental distinction being between microtasks and macrotasks. This prioritization system ensures that certain types of callbacks (like promise resolutions) are executed as soon as possible, while others (like setTimeout or I/O events) are deferred. Understanding this difference is crucial for predicting execution order and avoiding subtle bugs in asynchronous code.
Definition: Macrotasks are larger units of work that the event loop picks from its queue to execute one per iteration. Each macrotask runs to completion before moving to the next .
Examples: setTimeout, setInterval, setImmediate (Node.js), I/O operations, UI rendering (browser), requestAnimationFrame .
Execution Order: The event loop picks the oldest macrotask from the queue and executes it entirely. After it completes, it checks the microtask queue before proceeding to the next macrotask or rendering .
Creation Sources: Typically scheduled by the host environment (browser or Node.js) in response to external events, timers, or I/O completion .
Definition: Microtasks are smaller, high-priority tasks that need to be executed immediately after the currently executing script, before the next macrotask or rendering .
Examples: Promise callbacks (.then, .catch, .finally), MutationObserver, queueMicrotask(), process.nextTick (Node.js, though technically a separate queue with even higher priority) .
Execution Order: After every macrotask, the event loop processes the entire microtask queue until it's empty. If new microtasks are added during this processing, they are executed in the same cycle .
Creation Sources: Usually generated by JavaScript itself rather than external events, such as promise resolutions or explicit queueMicrotask calls .
The event loop algorithm for browsers and Node.js follows this pattern: execute the oldest macrotask → process all microtasks → perform rendering if needed → next macrotask. This ensures that microtask callbacks run before any I/O events, timers, or rendering, which is essential for promise-based APIs to maintain predictable state consistency. For example, when a promise resolves, its .then callback should run before any new network events are processed, preventing race conditions.
Priority: Microtasks always have higher priority than macrotasks. They run immediately after the current script completes, even before rendering .
Queue Processing: The event loop processes ONE macrotask per iteration, but processes ALL microtasks until the microtask queue is empty .
Recursive Microtasks: If a microtask schedules another microtask, the event loop will keep processing them in the same cycle, potentially starving macrotasks .
Rendering Interleaving: In browsers, rendering occurs between macrotask and microtask processing. If you need to update the UI after state changes, microtasks may delay rendering .
Error Handling: Errors in microtasks and macrotasks propagate differently. Unhandled promise rejections (microtasks) are treated specially, while macrotask errors can be caught with try/catch in the appropriate scope .
In Node.js, the distinction is slightly more nuanced. It uses multiple phases within its event loop: timers, I/O callbacks, idle/prepare, poll, check (setImmediate), and close callbacks. Microtasks (including process.nextTick which has its own queue with even higher priority) are processed between these phases. Specifically, after each phase, Node.js processes all process.nextTick callbacks, then all other microtasks (promise callbacks), before moving to the next phase.
For developers, the practical implication is that code using promises will generally run before code using setTimeout, even with zero delay. This is why microtasks are perfect for promise-based APIs (they execute as soon as possible) and why setTimeout is suitable for deferring work that shouldn't block the microtask queue. Understanding this queue prioritization helps in debugging race conditions and optimizing performance, especially when mixing promises with timers or I/O operations.